/*
 * BlackLightning.LoginClient
 * Andy Tidball
 * 
 * Project: Black Lightning
 * Copyright: GNU General Public License
 */

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.Serialization;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using BlackLightning.PluginLib;
using BlackLightning.PluginLib.LoginClients;
using BLLib.Configuration;

namespace BlackLightning {
	/// <summary>
	/// Wraps around an ILoginClient from an external app domain.
	/// </summary>
	internal class LoginClient : MarshalByRefObject, ILoginClient {
		// constants
		public const string FullnameSeparator = "."; // the separator used between a plugin's name and the login client's name in its full name

		// member data
		private string           _Name;          // the login client's name
		private string           _Filename;      // the filename of the login client's DLL assembly
		private AppDomain        _Domain;        // the app domain that all the login client's code is loaded into
		private LoginClientProxy _Proxy;         // the proxy of the login client that exists in the client's app domain
		private OptionSet        _PluginOptions; // options for all LoginClients in this plugin (basically just a placeholder)
		private OptionSet        _Options;       // the login client's option set
		private string           _Folder;        // the folder where the plugin can store its data

		/// <summary>
		/// Creates a new login client using the given filename.
		/// </summary>
		/// <param name="Filename">The name of the DLL file to load the login client from.</param>
		public LoginClient(string Filename) {
			// make sure this is a DLL file
			if (!Filename.ToLower().EndsWith(".dll")) {
				Filename += ".dll";
			}

			// save the name and filename
			_Filename = Filename;
			_Name     = Filename.Substring(0, Filename.Length-4);

			// find our data folder
			_Folder = BlackLightning.PreferencesPath + _Name;
			Directory.CreateDirectory(_Folder);

			// create our options
			_PluginOptions = null;
			_Options       = null;
		}

		/// <summary>
		/// Initializes the login client by actually loading the code from the external file.
		/// </summary>
		/// <param name="LoginClientName">The name of the login client to load.</param>
		/// <param name="Options">The option set to use for the login client.</param>
		public void Initialize(string LoginClientName, OptionSet Options) {
			// the given options are our plugin options
			_PluginOptions           = Options;
			_PluginOptions.Name      = _Name;
			_PluginOptions.IconIndex = BlackLightning.FileIconIndex;
			_PluginOptions.SelectedIconIndex = BlackLightning.FileIconIndex;

			// find the option set for the login client itself
			_Options = _PluginOptions.GetOrCreateOptionSet(LoginClientName);
			_Options.OptionValueChanged += OptionValueChanged;
			_Options.IconIndex = BlackLightning.KeyIconIndex;
			_Options.SelectedIconIndex = BlackLightning.KeyIconIndex;

			// create an app domain to load the plugin's functionality into
			_Domain = CreateAppDomain();

			// create a new proxy in the new app domain
			try {
				_Proxy = _Domain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "BlackLightning.LoginClientProxy") as LoginClientProxy;
				_Proxy.Initialize(BlackLightning.ApplicationPath + _Filename, LoginClientName);
			} catch (Exception x) {
				throw new LoadException("Unable to load login client '" + _Name + "' from filename: " + Filename, x);
			}

			// wire up the proxy's events to fire ours
			_Proxy.SendMessage += MessageReceived;

			// let the login client know that it was loaded
			_Proxy.HandleMessage(new LoadedMessage(_Folder));
		}

		/// <summary>
		/// Gets the name of the login client.
		/// </summary>
		public string Name {
			get {
				return _Name;
			}
		}

		/// <summary>
		/// Gets the filename of the login client.
		/// </summary>
		public string Filename {
			get {
				return _Filename;
			}
		}

		/// <summary>
		/// Gets the login client's full name.
		/// </summary>
		public string Fullname {
			get {
				return Filename + FullnameSeparator + Name;
			}
		}

		/// <summary>
		/// Gets the folder where the login client is allowed to store data files.
		/// </summary>
		public string DataFolder {
			get {
				return _Folder;
			}
		}

		/// <summary>
		/// Gets the login client's option set.
		/// </summary>
		public OptionSet Options {
			get {
				return _Options;
			}
		}
		
		/// <summary>
		/// Gets the login client's help text.
		/// </summary>
		public string HelpText {
			get {
				return _Proxy.GetProperty(LoginClientProperty.HelpText) as string;
			}
		}

		/// <summary>
		/// Gets the username that the user logged in with.
		/// </summary>
		public string Username {
			get {
				return _Proxy.GetProperty(LoginClientProperty.Username) as string;
			}
		}

		/// <summary>
		/// Gets the password that the user logged in with.
		/// </summary>
		public string Password {
			get {
				return _Proxy.GetProperty(LoginClientProperty.Password) as string;
			}
		}

		/// <summary>
		/// Gets the code for the game that the user logged into.
		/// </summary>
		public string GameCode {
			get {
				return _Proxy.GetProperty(LoginClientProperty.GameCode) as string;
			}
		}

		/// <summary>
		/// Gets the code for the character that the user logged in with.
		/// </summary>
		public string CharCode {
			get {
				return _Proxy.GetProperty(LoginClientProperty.CharCode) as string;
			}
		}

		/// <summary>
		/// Gets the code for the front end that the user selected.
		/// </summary>
		public string FECode {
			get {
				return _Proxy.GetProperty(LoginClientProperty.FECode) as string;
			}
		}
		
		/// <summary>
		/// Fired whenever the underlying login client fires its SendMessage event.
		/// </summary>
		public event SendMessageEventHandler SendMessage;

		/// <summary>
		/// Fires the SendMessage event.
		/// </summary>
		/// <param name="Sender">The login client that's sending the event.</param>
		/// <param name="e">Arguments to pass to the event.</param>
		private void OnSendMessage(SendMessageEventArgs e) {
			if (SendMessage != null) {
				SendMessage(this, e);
			}
		}
		
		/// <summary>
		/// Gets the value of the given property from the login client.
		/// </summary>
		/// <param name="Property">The property whose value to retrieve from the login client.</param>
		/// <returns>The value of the requested property.</returns>
		public object GetProperty(LoginClientProperty Property) {
			return _Proxy.GetProperty(Property);
		}
		
		/// <summary>
		/// Passes the given message down to the underlying logini client.
		/// </summary>
		/// <param name="Message">The message to pass to the underlying login client.</param>
		public void HandleMessage(Message Message) {
			_Proxy.HandleMessage(Message);
		}

		/// <summary>
		/// Runs the login logic from the underlying login client.
		/// </summary>
		/// <param name="Args">The command-line arguments passed to Black Lightning for use logging in.</param>
		/// <returns>The SAL file returned by the underlying login client.</returns>
		public SalFile Login(string[] Args) {
			return _Proxy.Login(Args);
		}

		/// <summary>
		/// Unloads the login client.
		/// </summary>
		public void Unload() {
			// let the login client know that we're being unloaded
			_Proxy.HandleMessage(new UnloadedMessage());

			// unwire its events from ours
			_Proxy.SendMessage -= MessageReceived;

			// stop listening for option values changing and uninitialize all of them
			_Options.OptionValueChanged -= OptionValueChanged;
			_Options.UninitiazeAll(false);

			// unload the proxy
			_Proxy.Unload();
			_Proxy = null;

			// unload the app domain
			AppDomain.Unload(_Domain);
			_Domain = null;
		}

		/// <summary>
		/// Called when the LeaseManager needs to acquire a Lease for how long we'll live on by ourselves.
		/// </summary>
		/// <returns>A Lease that will be used to control our lifetime.</returns>
		public override object InitializeLifetimeService() {
			// return null to signal an infinite amount of time
			return null;
		}

		/// <summary>
		/// Creates an app domain to load the external functionality of the login client into.
		/// </summary>
		/// <returns>The app domain to use to load external functionality into.</returns>
		private AppDomain CreateAppDomain() {
			// add our application directory to the host evidence for the app domain
			Evidence E = new Evidence(AppDomain.CurrentDomain.Evidence);
			E.AddHost(new ApplicationDirectory(BlackLightning.ApplicationPath));

			// create a new app domain with the evidence
			AppDomain Domain = AppDomain.CreateDomain(_Name, E);

			// start with our default permissions
			PermissionSet PS = new PermissionSet(BlackLightning.DefaultPermissions);

			// add file access permissions to our folder
			PS.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess, _Folder));

			// add network permissions if the user has allowed them
			PS.AddPermission(new DnsPermission(PermissionState.Unrestricted));
			PS.AddPermission(new SocketPermission(NetworkAccess.Connect, TransportType.Tcp, PluginLib.LoginClients.LoginClient.LoginServerHost, PluginLib.LoginClients.LoginClient.LoginServerPort));

			// set the domain's security policy to include the permissions we created
			Domain.SetAppDomainPolicy(BlackLightning.CreateSecurityPolicy(PS));

			// return the new app domain
			return Domain;
		}

		/// <summary>
		/// Handles the underlying login client's SendMessage event.
		/// </summary>
		/// <param name="Sender">The underlying login client.</param>
		/// <param name="e">Arguments about the event.</param>
		private void MessageReceived(object Sender, SendMessageEventArgs e) {
			if (e.Message is RegisterOptionMessage) {
				// this is an option registration, we can handle that
				MessageReceived_RegisterOption(e.Message as RegisterOptionMessage);
			} else if (e.Message is UnregisterOptionMessage) {
				// this is an option unregistration, we can handle that
				MessageReceived_UnregisterOption(e.Message as UnregisterOptionMessage);
			} else if (e.Message is ChangeOptionValueMessage) {
				// they want to change an option's value, we can handle that
				MessageReceived_ChangeOptionValue(e.Message as ChangeOptionValueMessage);
			} else {
				// fire the event further upwards
				OnSendMessage(e);
			}
		}
		
		/// <summary>
		/// Handles the underlying login client sending a RegisterOptionMessage.
		/// </summary>
		/// <param name="Message">The message sent by the underlying login client.</param>
		private void MessageReceived_RegisterOption(RegisterOptionMessage Message) {
			// initialize the option as per the message
			Option O = _Options.GetOrCreateOption(Message.Name);
			O.Initialize(Message.GetInitializer(new int[]{BlackLightning.GlobalOptionValueIndex, BlackLightning.UserOptionValueIndex}));
		}

		/// <summary>
		/// Handles the underlying login client sending a RegisterOptionMessage.
		/// </summary>
		/// <param name="Message">The message sent by the underlying login client.</param>
		private void MessageReceived_UnregisterOption(UnregisterOptionMessage Message) {
			// make sure that we have an option by that name
			if (!_Options.HasOption(Message.Name)) {
				// send an error message to the login client
				_Proxy.HandleMessage(new ErrorMessage(Message, new ArgumentException("There is no registered option with the given name.")));
				return;
			}

			// remove the option
			_Options.RemoveOption(Message.Name);
		}

		/// <summary>
		/// Handles the underlying login client sending a ChangeOptionValueMessage.
		/// </summary>
		/// <param name="Message">The message sent by the underlying login client.</param>
		private void MessageReceived_ChangeOptionValue(ChangeOptionValueMessage Message) {
			// make sure that we actually have an option by that name
			if (!_Options.HasOption(Message.Name)) {
				// send an error message to the login client
				_Proxy.HandleMessage(new ErrorMessage(Message, new ArgumentException("There is no registered option with the given name.")));
				return;
			}

			// grab the option and set its value
			try {
				Option O = _Options.GetOption(Message.Name);
				O.Value = Message.Value;
				O.ApplyNewValue();
			} catch (Exception x) {
				// send an error message to the login client
				_Proxy.HandleMessage(new ErrorMessage(Message, x));
			}
		}

		/// <summary>
		/// Handles any of the login client's option's value changing.
		/// </summary>
		/// <param name="Sender">The option whose value changed.</param>
		/// <param name="e">Arguments about the event.</param>
		public void OptionValueChanged(object Sender, OptionValueChangedEventArgs e) {
			// unless this is a change in the lowest value index, we don't care
			Option O = Sender as Option;
			if (e.ValueIndex != O.LowestValueIndex) {
				return;
			}

			// let the login client know that an option value changed
			_Proxy.HandleMessage(new OptionValueChangedMessage(O.Name, e.OldValue, e.NewValue));
		}
	}

	/// <summary>
	/// Represents a loaded login client in its own app domain.
	/// </summary>
	internal class LoginClientProxy : MarshalByRefObject, ILoginClient {
		// member data
		private Assembly     _Assembly;    // the assembly that the external functionality resides in
		private ILoginClient _LoginClient; // the external login client's functionality

		/// <summary>
		/// Creates a new LoginClientProxy.
		/// </summary>
		public LoginClientProxy() {
		}

		/// <summary>
		/// Loads up the external functioinality from the assembly and loads the given login client type from it.
		/// </summary>
		/// <param name="AssemblyName">The name of the assembly file to load.</param>
		/// <param name="LoginClientName">The name of the login client to load.</param>
		public void Initialize(string AssemblyName, string LoginClientName) {
			// load up the assembly
			_Assembly = Assembly.LoadFrom(AssemblyName);

			// find the login client type we're looking for
			object[] LoginClientAttributes = _Assembly.GetCustomAttributes(typeof(BLLoginClientAttribute), true);
			Type LoginClientType = null;
			foreach (BLLoginClientAttribute A in LoginClientAttributes) {
				// find the name of this type
				string Name = A.Type.Name;

				// check if its type name is overridden by an attribute
				object[] NameAttributes = A.Type.GetCustomAttributes(typeof(LoginClientNameAttribute), true);
				if (NameAttributes.Length > 0) {
					Name = ((LoginClientNameAttribute)NameAttributes[0]).Name;
				}

				// if the name matches the one we want to load, we're done
				if (Name == LoginClientName) {
					LoginClientType = A.Type;
					break;
				}
			}

			// make sure we found a type
			if (LoginClientType == null) {
				throw new LoadException("No login client named '" + LoginClientName + "' was found in the assembly: " + AssemblyName);
			}

			// validate the type we found
			ValidateLoginClient(LoginClientType);

			// create an instance of the type
			try {
				// the type has already been validated to contain a default constructor
				object Instance = LoginClientType.GetConstructor(new Type[0]).Invoke(new Object[0]);
				if (!(Instance is ILoginClient)) {
					// this should never happen, it should be impossible for A.Type to not be a type derived from ILoginClient
					throw new LoadException("Constructing an instance of type '" + LoginClientType.Name + "' resulted in a type which is not derived from ILoginClient.");
				}
				_LoginClient = Instance as ILoginClient;
			} catch (System.Reflection.TargetInvocationException x) {
				throw new LoadException("The login client threw an exception during construction.", x);
			}

			// wire up its event handler to fire ours
			_LoginClient.SendMessage += MessageReceived;
		}

		/// <summary>
		/// Fired whenever the underlying login client fires its SendMessage event.
		/// </summary>
		public event SendMessageEventHandler SendMessage;

		/// <summary>
		/// Fires the SendMessage event.
		/// </summary>
		/// <param name="e">Arguments to pass to the event.</param>
		private void OnSendMessage(SendMessageEventArgs e) {
			if (SendMessage != null) {
				SendMessage(this, e);
			}
		}

		/// <summary>
		/// Gets the value of the given property from the login client.
		/// </summary>
		/// <param name="Property">The property whose value to retrieve from the login client.</param>
		/// <returns>The value of the requested property.</returns>
		public object GetProperty(LoginClientProperty Property) {
			if (_LoginClient == null) {
				return null;
			}

			return _LoginClient.GetProperty(Property);
		}

		/// <summary>
		/// Passes the given message down to the underlying login client.
		/// </summary>
		/// <param name="Message">The message to pass to the underlying login client.</param>
		public void HandleMessage(Message Message) {
			if (_LoginClient != null) {
				_LoginClient.HandleMessage(Message);
			}
		}

		/// <summary>
		/// Runs the login logic from the underlying login client.
		/// </summary>
		/// <param name="Args">The command-line arguments passed to Black Lightning for use logging in.</param>
		/// <returns>The SAL file returned by the underlying login client.</returns>
		public SalFile Login(string[] Args) {
			return _LoginClient.Login(Args);
		}

		/// <summary>
		/// Unloads the login client.
		/// </summary>
		public void Unload() {
			// if there's a login client, unwire its events from ours
			if (_LoginClient != null) {
				_LoginClient.SendMessage -= MessageReceived;
			}

			// free our references
			_Assembly    = null;
			_LoginClient = null;
		}

		/// <summary>
		/// Called when the LeaseManager needs to acquire a Lease for how long we'll live on by ourselves.
		/// </summary>
		/// <returns>A Lease that will be used to control our lifetime.</returns>
		public override object InitializeLifetimeService() {
			// return null to signal an infinite amount of time
			return null;
		}

		/// <summary>
		/// Validates the given type as an ILoginClient.
		/// </summary>
		/// <param name="T">The type to validate as an ILoginClient.</param>
		/// <exception cref="LoadException">Thrown if the type fails to validate.</exception>
		private void ValidateLoginClient(Type T) {
			// check if this type implements the login client interface
			Type[] Interfaces = T.FindInterfaces(System.Reflection.Module.FilterTypeName, typeof(ILoginClient).Name);
			if (Interfaces.Length < 1) {
				// the type doesn't even claim to implement the interface
				throw new LoadException("The type '" + T.Name + "' does not implement the ILoginClient interface.");
			}
			if (Interfaces.Length > 1) {
				// the type doesn't even claim to implement the interface
				throw new LoadException("The type '" + T.Name + "' appears to implement the ILoginClient interface more than once.");
			}

			// make sure the type has a default constructor
			ConstructorInfo Constructor = T.GetConstructor(new Type[0]);
			if ((Constructor == null) || (Constructor.IsAbstract) || (!Constructor.IsPublic) || (Constructor.IsStatic)) {
				throw new LoadException("The type '" + T.Name + "' does not support a default constructor.");
			}
		}

		/// <summary>
		/// Handles the underlying login client's SendMessage event.
		/// </summary>
		/// <param name="Sender">The underlying login client.</param>
		/// <param name="e">Arguments about the event.</param>
		private void MessageReceived(object Sender, SendMessageEventArgs e) {
			// fire our own SendMessage event
			OnSendMessage(e);
		}
	}
}
